﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using Telerik.Windows.Documents.Utilities;

namespace BezierApproximations
{
    internal static class ArcHelper
    {
        private static readonly Point[] clockwiseAxisPoints;
        private const double Epsilon = 1E-6;

        static ArcHelper()
        {
            clockwiseAxisPoints = new Point[]
            {
                new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1)
            };
        }

        public static IEnumerable<Tuple<Point, Point, Point, Point>> GetEllipticArcCubicBezierApproximation(
            Point start, Point end, Size ellipseRadiuses, bool isLargeArc, SweepDirection sweepDirection, double xAxisRotationInDegrees)
        {
            Point segmentRadiusVector = end.Minus(start);
            xAxisRotationInDegrees = NormalizeAngle(xAxisRotationInDegrees);
            RotateTransform vectorRotation = new RotateTransform() { Angle = NormalizeAngle(-xAxisRotationInDegrees) };
            segmentRadiusVector = vectorRotation.Transform(segmentRadiusVector);

            Point segmentVector = HandleSmallCoordinates(segmentRadiusVector);

            Tuple<Point, Point> ellipseIntersections = ArcHelper.GetEllipseArcEndPoints(ellipseRadiuses, segmentVector, isLargeArc, sweepDirection);

            if (ellipseIntersections != null)
            {
                ellipseIntersections = new Tuple<Point, Point>(HandleSmallCoordinates(ellipseIntersections.Item1), HandleSmallCoordinates(ellipseIntersections.Item2));

                Point actualEllipseVector = ellipseIntersections.Item2.Minus(ellipseIntersections.Item1);
                double segmentLengthScaleFactor = segmentVector.Length() / actualEllipseVector.Length();
                Size ellipseToCircleScale = ArcHelper.GetScaleFactorFromEllipseToBoundingCircle(ellipseRadiuses);

                Matrix circleToTranslatedEllipseTransform = new Matrix()
                .ScaleMatrix(1 / ellipseToCircleScale.Width, 1 / ellipseToCircleScale.Height)
                .TranslateMatrix(-ellipseIntersections.Item1.X, -ellipseIntersections.Item1.Y)
                .ScaleMatrix(segmentLengthScaleFactor, segmentLengthScaleFactor)
                .RotateMatrix(xAxisRotationInDegrees)
                .TranslateMatrix(start.X, start.Y);

                Tuple<Point, Point> pointsOnCircle = ArcHelper.TranslatePointsToBoundingCircle(ellipseRadiuses, ellipseIntersections);

                foreach (Tuple<Point, Point> smallArc in ArcHelper.DivideCircleArcToSmallerArcs(pointsOnCircle, sweepDirection, isLargeArc))
                {
                    var circleBezierPoints = ArcHelper.GetArcCubicBezierApproximation(smallArc, sweepDirection);
                    var ellipseArcBezierPoints = new Tuple<Point, Point, Point, Point>(
                        circleToTranslatedEllipseTransform.Transform(circleBezierPoints.Item1),
                        circleToTranslatedEllipseTransform.Transform(circleBezierPoints.Item2),
                        circleToTranslatedEllipseTransform.Transform(circleBezierPoints.Item3),
                        circleToTranslatedEllipseTransform.Transform(circleBezierPoints.Item4));

                    yield return ellipseArcBezierPoints;
                }
            }

            yield break;
        }

        internal static Tuple<Point, Point, Point, Point> GetArcCubicBezierApproximation(Tuple<Point, Point> arcPoints, SweepDirection sweepDirection)
        {
            double angle = GetArcAngle(arcPoints);
            double kappa = GetCubicBezierArcKappa(angle);

            ScaleTransform scale = new ScaleTransform() { ScaleX = kappa, ScaleY = kappa };
            RotateTransform firstRotation = new RotateTransform() { Angle = sweepDirection == SweepDirection.Clockwise ? 90 : -90 };
            RotateTransform secondRotation = new RotateTransform() { Angle = sweepDirection == SweepDirection.Clockwise ? -90 : 90 };

            Point firstTangentVector = firstRotation.Transform(arcPoints.Item1);
            firstTangentVector = scale.Transform(firstTangentVector);
            Point secondTangentVector = secondRotation.Transform(arcPoints.Item2);
            secondTangentVector = scale.Transform(secondTangentVector);

            Tuple<Point, Point, Point, Point> bezierPoints = new Tuple<Point, Point, Point, Point>(
                arcPoints.Item1,
                arcPoints.Item1.Plus(firstTangentVector),
                arcPoints.Item2.Plus(secondTangentVector),
                arcPoints.Item2);

            return bezierPoints;
        }

        internal static IEnumerable<Tuple<Point, Point>> DivideCircleArcToSmallerArcs(Tuple<Point, Point> arcPoints, SweepDirection sweepDirection, bool isLargeArc)
        {
            Point current = arcPoints.Item1;
            double radius = Math.Sqrt(current.X * current.X + current.Y * current.Y);
            ScaleTransform radiusTransform = new ScaleTransform() { ScaleX = radius, ScaleY = radius };
            Point secondAxisPoint = GetClosestAxisPoint(arcPoints.Item2, sweepDirection);
            bool isFirstSegmentOnLargeArc = isLargeArc;

            Point nextCurrentAxisPoint = GetNextAxisPoint(current, sweepDirection);
            while (!nextCurrentAxisPoint.Equals(secondAxisPoint) || isFirstSegmentOnLargeArc)
            {
                Point scaledAxisPoint = radiusTransform.Transform(nextCurrentAxisPoint);
                yield return new Tuple<Point, Point>(current, scaledAxisPoint);

                current = scaledAxisPoint;
                nextCurrentAxisPoint = GetNextAxisPoint(current, sweepDirection);
                isFirstSegmentOnLargeArc = false;
            }

            yield return new Tuple<Point, Point>(current, arcPoints.Item2);
        }

        internal static Tuple<Point, Point> TranslatePointsToBoundingCircle(Size ellipse, Tuple<Point, Point> ellipsePoints)
        {
            Size scaleFactor = GetScaleFactorFromEllipseToBoundingCircle(ellipse);
            ScaleTransform scaleTransform = new ScaleTransform() { ScaleX = scaleFactor.Width, ScaleY = scaleFactor.Height };
            Tuple<Point, Point> circlePoints = new Tuple<Point, Point>(scaleTransform.Transform(ellipsePoints.Item1), scaleTransform.Transform(ellipsePoints.Item2));

            return circlePoints;
        }

        internal static Tuple<Point, Point> GetEllipseArcEndPoints(Size ellipse, Point segmentVector, bool isLargeArc, SweepDirection sweepDirection)
        {
            if (segmentVector.Y == 0)
            {
                return GetHorizontalArcEndPoints(ellipse, segmentVector, isLargeArc, sweepDirection);
            }
            else
            {
                return GetNotHorizontalArcEndPoints(ellipse, segmentVector, isLargeArc, sweepDirection);
            }
        }

        private static double GetArcAngle(Tuple<Point, Point> arcPoints)
        {
            Point a = arcPoints.Item1;
            Point b = arcPoints.Item2;
            double scalar = a.MultiplyBy(b);
            double aLength = Math.Sqrt(a.MultiplyBy(a));
            double bLength = Math.Sqrt(b.MultiplyBy(b));

            double angle = Math.Acos(scalar / (aLength * bLength));

            return angle;
        }

        private static Point GetNextAxisPoint(Point point, SweepDirection sweepDirection)
        {
            Point closestAxisPoint = GetClosestAxisPoint(point, sweepDirection);

            if (point.X != 0 && point.Y != 0)
            {
                return closestAxisPoint;
            }

            int index = Array.IndexOf(clockwiseAxisPoints, closestAxisPoint);
            int deltaIndex = sweepDirection == SweepDirection.Clockwise ? +1 : -1;
            index = index + deltaIndex;
            if (index == -1)
            {
                index = clockwiseAxisPoints.Length - 1;
            }
            else if (index == clockwiseAxisPoints.Length)
            {
                index = 0;
            }

            Point nextAxisPoint = clockwiseAxisPoints[index];

            return nextAxisPoint;
        }

        private static Point GetClosestAxisPoint(Point point, SweepDirection sweepDirection)
        {
            if (point == new Point())
            {
                throw new ArgumentException("point cannot be zero!");
            }

            if (point.X > 0 && point.Y > 0)
            {
                return sweepDirection == SweepDirection.Clockwise ? new Point(0, 1) : new Point(1, 0);
            }
            else if (point.X > 0 && point.Y < 0)
            {
                return sweepDirection == SweepDirection.Clockwise ? new Point(1, 0) : new Point(0, -1);
            }
            else if (point.X < 0 && point.Y < 0)
            {
                return sweepDirection == SweepDirection.Clockwise ? new Point(0, -1) : new Point(-1, 0);
            }
            else if (point.X < 0 && point.Y > 0)
            {
                return sweepDirection == SweepDirection.Clockwise ? new Point(-1, 0) : new Point(0, 1);
            }

            return new Point(Math.Sign(point.X), Math.Sign(point.Y));
        }

        private static Size GetScaleFactorFromEllipseToBoundingCircle(Size ellipse)
        {
            double x = ellipse.Width > ellipse.Height ? 1 : ellipse.Height / ellipse.Width;
            double y = ellipse.Width > ellipse.Height ? ellipse.Width / ellipse.Height : 1;

            return new Size(x, y);
        }

        private static Tuple<Point, Point> GetHorizontalArcEndPoints(Size ellipse, Point segmentVector, bool isLargeArc, SweepDirection sweepDirection)
        {
            if (segmentVector.Y != 0)
            {
                throw new ArgumentException("segment vector must be horizontal!");
            }

            double segmentVectorX = segmentVector.X;
            double[] yAxisIntersections;
            double yAxisIntersection;

            if (TryGetYAxisIntersection(ellipse, segmentVectorX, out yAxisIntersections))
            {
                yAxisIntersection = ShouldTakeTopIntersection(segmentVectorX, isLargeArc, sweepDirection) ? yAxisIntersections[0] : yAxisIntersections[1];
            }
            else
            {
                yAxisIntersection = 0;
            }

            Tuple<Point, Point> ellipseCrossPoints = GetHorizontalCrossPoints(ellipse, segmentVectorX, yAxisIntersection);

            return ellipseCrossPoints;
        }

        private static Tuple<Point, Point> GetNotHorizontalArcEndPoints(Size ellipse, Point segmentVector, bool isLargeArc, SweepDirection sweepDirection)
        {
            double[] xAxisIntersections;
            double xAxisIntersection;

            if (TryGetXAxisIntersection(ellipse, segmentVector, out xAxisIntersections))
            {
                xAxisIntersection = ShouldTakeLeftIntersection(segmentVector, isLargeArc, sweepDirection) ? xAxisIntersections[0] : xAxisIntersections[1];
            }
            else
            {
                xAxisIntersection = 0;
            }

            Tuple<Point, Point> ellipseCrossPoints = GetCrossPoints(ellipse, segmentVector, xAxisIntersection);

            return ellipseCrossPoints;
        }

        private static bool ShouldTakeTopIntersection(double segmentVectorX, bool isLargeArc, SweepDirection sweepDirection)
        {
            if (segmentVectorX == 0)
            {
                throw new ArgumentException("segmentVector cannot be zero");
            }

            bool largeArcScenario;
            bool smallArcScenario;

            if (segmentVectorX > 0)
            {
                largeArcScenario = isLargeArc && sweepDirection == SweepDirection.Counterclockwise;
                smallArcScenario = !isLargeArc && sweepDirection == SweepDirection.Clockwise;
            }
            else
            {
                largeArcScenario = isLargeArc && sweepDirection == SweepDirection.Clockwise;
                smallArcScenario = !isLargeArc && sweepDirection == SweepDirection.Counterclockwise;
            }

            return largeArcScenario || smallArcScenario;
        }

        private static bool ShouldTakeLeftIntersection(Point segmentVector, bool isLargeArc, SweepDirection sweepDirection)
        {
            if (segmentVector.Y == 0)
            {
                throw new ArgumentException("segmentVector cannot be horizontal");
            }

            bool largeArcScenario;
            bool smallArcScenario;

            if (segmentVector.Y > 0)
            {
                largeArcScenario = isLargeArc && sweepDirection == SweepDirection.Clockwise;
                smallArcScenario = !isLargeArc && sweepDirection == SweepDirection.Counterclockwise;
            }
            else
            {
                largeArcScenario = isLargeArc && sweepDirection == SweepDirection.Counterclockwise;
                smallArcScenario = !isLargeArc && sweepDirection == SweepDirection.Clockwise;
            }

            return largeArcScenario || smallArcScenario;
        }

        private static double GetCubicBezierArcKappa(double arcAngleInRadians)
        {
            if (arcAngleInRadians <= 0 || arcAngleInRadians > Math.PI / 2)
            {
                throw new ArgumentOutOfRangeException("arcAngleInRadians must be in the interval (0; Math.PI/2]");
            }

            double sin = Math.Sin(arcAngleInRadians);
            double cos = Math.Cos(arcAngleInRadians);
            double halfCos = Math.Cos(arcAngleInRadians / 2);

            double kappa = (8 * halfCos - 4 - 4 * cos) / (3 * sin);

            return kappa;
        }

        private static bool TryGetYAxisIntersection(Size ellipse, double segmentVectorXCoordinate, out double[] yAxisIntersections)
        {
            yAxisIntersections = null;
            double v = segmentVectorXCoordinate;
            double a = ellipse.Width;
            double b = ellipse.Height;

            double squared = 4 * a * a - v * v;

            if (squared > 0)
            {
                double result = Math.Sqrt(squared) * b / (2 * a);
                yAxisIntersections = new double[] { -result, result };

                return true;
            }

            return false;
        }

        private static bool TryGetXAxisIntersection(Size ellipse, Point segmentVector, out double[] xAxisIntersections)
        {
            xAxisIntersections = null;
            double v1 = segmentVector.X;
            double v2 = segmentVector.Y;
            double a = ellipse.Width;
            double b = ellipse.Height;

            double k = v1 * v1 * b * b + v2 * v2 * a * a;

            double squared = (k * k - 4 * k * a * a * b * b) / (4 * b * b * (v1 * v1 * b * b - k));

            if (squared > 0)
            {
                double sqrt = Math.Sqrt(squared);
                xAxisIntersections = new double[] { -sqrt, +sqrt };

                return true;
            }

            return false;
        }

        private static Tuple<Point, Point> GetHorizontalCrossPoints(Size ellipse, double segmentVectorX, double yAxisIntersection)
        {
            double v = segmentVectorX;
            double p = yAxisIntersection;
            double a = ellipse.Width;
            double b = ellipse.Height;

            double squared = (b * b - p * p) / (v * v);
            if (squared <= 0)
            {
                return null;
            }

            double tResult = Math.Sqrt(squared) * a / b;

            double t1 = -tResult;
            double t2 = tResult;

            return new Tuple<Point, Point>(new Point(t1 * v, p), new Point(t2 * v, p));
        }

        private static Tuple<Point, Point> GetCrossPoints(Size ellipse, Point segmentVector, double xAxisIntersection)
        {
            double v1 = segmentVector.X;
            double v2 = segmentVector.Y;
            double a = ellipse.Width;
            double b = ellipse.Height;
            double p = xAxisIntersection;

            double k = v1 * v1 * b * b + v2 * v2 * a * a;

            double d = b * b * (v1 * v1 * p * p * b * b - k * p * p + k * a * a);
            if (d <= 0)
            {
                return null;
            }

            double c = -v1 * p * b * b;
            double sqrtD = Math.Sqrt(d);

            Func<double, Point> getPointFromLine = (t) =>
            {
                return new Point(t * v1 + p, t * v2);
            };

            double t1 = (c - sqrtD) / k;
            double t2 = (c + sqrtD) / k;

            return new Tuple<Point, Point>(getPointFromLine(t1), getPointFromLine(t2));
        }

        private static Point HandleSmallCoordinates(Point point)
        {
            return new Point(HandleSmallNumber(point.X), HandleSmallNumber(point.Y));
        }

        private static double HandleSmallNumber(double number)
        {
            if (Math.Abs(number) < Epsilon)
            {
                return 0;
            }

            return number;
        }

        private static double NormalizeAngle(double angleInDegrees)
        {
            angleInDegrees = angleInDegrees % 360;

            if (angleInDegrees < 0)
            {
                angleInDegrees = 360 + angleInDegrees;
            }

            return angleInDegrees;
        }
    }
}
